package datatypes;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static tools.Tool.leftOfTest;
import datatypes.kdTree.Range;

/**
 * Describes a triangle via 3 connected edges
 * @author markessien
 *
 */
public class Triangle implements Comparable<Triangle> {
	
	Edge edge1;
	Edge edge2;
	Edge edge3;
	
	public Triangle(Edge e1, Edge e2, Edge e3, boolean checkForCorrectness) throws Exception {
		
		if (checkForCorrectness) {
			if (e1 == null) {
				throw new NullPointerException("e1 is null");
			}
			if (e2 == null) {
				throw new NullPointerException("e2 is null");
			}
			if (e3 == null) {
				throw new NullPointerException("e3 is null");
			}
			if (e2.hasVertex(e1.getVertex1())) {
				if ( ! e3.hasVertex(e1.getVertex2())) {
					throw new Exception("e3 does not share vertex with e1 or shares the same vertex with e1 as e2.");
				}
				if ( ! e3.hasVertex(e2.getOtherVertex(e1.getVertex1()))) {
					throw new Exception("e3 does not share vertex with e2 or shares the same vertex with e2 as e1.");
				}
			} else if (e2.hasVertex(e1.getVertex2())) {
				if ( ! e3.hasVertex(e1.getVertex1())) {
					throw new Exception("e3 does not share vertex with e1 or shares the same vertex with e1 as e2.");
				}
				if ( ! e3.hasVertex(e2.getOtherVertex(e1.getVertex2()))) {
					throw new Exception("e3 does not share vertex with e2 or shares the same vertex with e2 as e1.");
				}
			} else {
				throw new Exception("e2 does not share vertex with e1!");
			}
		}
		this.edge1 = e1;
		this.edge2 = e2;
		this.edge3 = e3;
	}
	
	public Edge getEdge1() {
		return edge1;
	}
	
	public Edge getEdge2() {
		return edge2;
	}
	
	public Edge getEdge3() {
		return edge3;
	}
	
	public Vertex getVertex1() {
		return edge1.getVertex1();
	}
	
	public Vertex getVertex2() {
		return edge1.getVertex2();
	}
	
	public Vertex getVertex3() {
		return getVertexNotOnEdge(edge1);
	}
	
	public Range getBoundingBox() {
		return getBoundingBox(getVertex1(), getVertex2(), getVertex3());
	}
	
	public static Range getBoundingBox(Vertex v1, Vertex v2, Vertex v3) {
		double v1x = v1.getX();
		double v2x = v2.getX();
		double v3x = v3.getX();
		double v1y = v1.getY();
		double v2y = v2.getY();
		double v3y = v3.getY();
		double minX = min(v1x, v2x);
		minX = min(minX, v3x);
		double minY = min(v1y, v2y);
		minY = min(minY, v3y);
		
		double maxX = max(v1x, v2x);
		maxX = max(maxX, v3x);
		double maxY = max(v1y, v2y);
		maxY = max(maxY, v3y);
		
		return new Range(minX, maxX, minY, maxY);
		
	}
	
	public boolean containsEdge(Edge edge) {
		if (edge == edge1) {
			return true;
		}
		if (edge == edge2) {
			return true;
		}
		if (edge == edge3) {
			return true;
		}
		return false;
	}
	
	/**
	 * Get the weight of the triangle. This base implementation simply
	 * sums up the weights of the edges
	 * 
	 * @return The weight of this triangle
	 */
	public double getCircumferenceLength() {
		return edge1.getLength() + edge2.getLength() + edge3.getLength();
	}
	
	
	/**
	 * This function allows us get the edges in a consistent manner. A
	 * reference edge is provided which acts as 'edge 1'. The edge connected
	 * to vertex1 of edge1 is then 'edge 2' and the edge connected to 
	 * vertex2 is then 'edge 3'. This allows us to always find the same
	 * edges no matter how the triangle is rotated
	 * 
	 * @param edgeNumber The edge number to get.
	 * @param referenceEdge The edge identified as 'Edge 1'
	 * @return The edge requested or null
	 */
	public Edge getEdge(int edgeNumber, Edge referenceEdge) {
		if (referenceEdge != edge1 && referenceEdge != edge2 && referenceEdge != edge3) {
			System.err.println("edge is not member of this triangle!");
			return null;
		}
		
		if (edgeNumber == 1)
			return referenceEdge;
		
		if (edgeNumber == 2) {
			Vertex v1 = referenceEdge.getVertex1();
			if (edge1.hasVertex(v1) && edge1 != referenceEdge)
				return edge1;
			
			if (edge2.hasVertex(v1) && edge2 != referenceEdge)
				return edge2;
			
			if (edge3.hasVertex(v1) && edge3 != referenceEdge)
				return edge3;
		}
		
		if (edgeNumber == 3) {
			Vertex v2 = referenceEdge.getVertex2();
			if (edge1.hasVertex(v2) && edge1 != referenceEdge)
				return edge1;
			
			if (edge2.hasVertex(v2) && edge2 != referenceEdge)
				return edge2;
			
			if (edge3.hasVertex(v2) && edge3 != referenceEdge)
				return edge3;
		}
		
		return null;
	}
	
	/**
	 * Tests whether a triangle is empty with respect to the list of vertices
	 * passed in. Each of the vertices is tested, the first vertex found to
	 * be inside this triangle causes this method to terminate returning false. 
	 * 
	 * @param points The points to be tested to see if in this triangle
	 * @return true if triangle empty, otherwise false
	 */
	public boolean isEmpty(Iterable<Vertex> vertices, double epsilon) {
		for (Vertex vertex : vertices) {
			if ( vertex.isInside(this, epsilon) ) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Tests whether a triangle is empty with respect to the vertices of the set
	 * passed in. Each of the vertices is tested, the first vertex found to
	 * be inside this triangle causes this method to terminate returning false. 
	 * 
	 * @param set An IndexedFaceSet containing vertices to be tested to see if in this triangle
	 * @return true if triangle empty, otherwise false
	 */
	public boolean isEmpty(IndexedFaceSet set, double epsilon) {
		Range range = getBoundingBox();
		return isEmpty(set.vertexRangeIterable(range), epsilon);
	}
	
	/**
	 * Tests whether a triangle defined by the three vertices is empty with respect to the vertices of the set
	 * passed in. Each of the vertices is tested, the first vertex found to
	 * be inside the triangle causes this method to terminate returning false.
	 * 
	 * See {@link #isEmptyWithPretest(IndexedFaceSet, Vertex, Vertex, Vertex, double)} for
	 * an optimized version of this method.
	 * @param set
	 * @param v1
	 * @param v2
	 * @param v3
	 * @param epsilon
	 * @return
	 */
	public static boolean isEmpty(IndexedFaceSet set, Vertex v1, Vertex v2, Vertex v3, double epsilon) {
		Range range = getBoundingBox(v1, v2, v3);
		return isEmpty(set.vertexRangeIterable(range), v1, v2, v3, epsilon);
	}
	
	/**
	 * Tests whether a triangle defined by the three vertices is empty with respect to the vertices of the set
	 * passed in. Each of the vertices is tested, the first vertex found to
	 * be inside the triangle causes this method to terminate returning false.
	 * 
	 * This method is running a pretest checking a smaller range that is mostly
	 * within the triangle first resulting in fewer vertices with a higher
	 * likelihood to be within the triangle to be checked in the pretest.
	 * only if none of the pretest vertices are found to be within the whole
	 * set of vertices in the bounding box need to be checked.
	 * 
	 * Worst case performance is worse than {@link #isEmpty(IndexedFaceSet, Vertex, Vertex, Vertex, double)},
	 * but the average speedup is quite noticeable.
	 * @param set
	 * @param v1
	 * @param v2
	 * @param v3
	 * @param epsilon
	 * @return
	 */
	public static boolean isEmptyWithPretest(IndexedFaceSet set, Vertex v1, Vertex v2, Vertex v3, double epsilon) {
		Vertex offset = v2.getMinus(v1);
		offset.multiplyWithScalar(0.5);
		Vertex midpoint = v1.getPlus(offset);
		
		/* TODO: I'm suer there's a much smarter way to determine a pretest-range!
		 * Right now my range is not even guaranteed to be fully within the triangle...
		 */
		
		Range range = new Range(min(v3.getX(), midpoint.getX()), max(v3.getX(), midpoint.getX()), min(v3.getY(), midpoint.getY()), max(v3.getY(), midpoint.getY()));
		if (!isEmpty(set.vertexRangeIterable(range), v1, v2, v3, epsilon)) {
			return false;
		}
		range = getBoundingBox(v1, v2, v3);
		return isEmpty(set.vertexRangeIterable(range), v1, v2, v3, epsilon);
	}
	
	public static boolean isEmpty(Iterable<Vertex> vertices, Vertex v1, Vertex v2, Vertex v3, double epsilon) {
		for (Vertex vertex : vertices) {
			if (vertex.isInside(v1, v2, v3, epsilon)) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Given one of the edges in this triangle, this method will
	 * retrieve the point that does not belong to that edge
	 * 
	 * @param e The edge to be examines
	 * @return The other point
	 */
	public Vertex getVertexNotOnEdge(Edge e) {
				
		// Get the other two edges
		Edge e1 = getEdge(2, e);
		Edge e2 = getEdge(3, e);
		
		if (e1 == null) {
			System.err.println("e1 is null.");
			return null;
		}
		if (e2 == null) {
			System.err.println("e2 is null.");
			return null;
		}
		
		if (e1.getVertex1() == e2.getVertex1() || e1.getVertex1() == e2.getVertex2() ) {
			return e1.getVertex1();
		}
			
		
		if (e1.getVertex2() == e2.getVertex1() || e1.getVertex2() == e2.getVertex2() ) {
			return e1.getVertex2();
		}
		
		return null;
	}
	
	/**
	 * Given one of the edges of this triangle, this method returns the
	 * distance of the third point from that edge.
	 * @param base
	 * @return
	 */
	public double height(Edge base) {
		Vertex peak = this.getVertexNotOnEdge(base);
		return (Math.abs(leftOfTest(base, peak))/base.getLength());
	}

	/**
	 * Compares edge1. If zero, compares edge2. If still zero, compares edge3.
	 * @param other
	 * @return
	 */
	@Override
	public int compareTo(Triangle other) {
		int comparison = this.edge1.compareTo(other.edge1);
		if (comparison == 0) {
			comparison = this.edge2.compareTo(other.edge2);
		}
		if (comparison == 0) {
			comparison = this.edge3.compareTo(other.edge3);
		}
		return comparison;
	}
	
	@Override
	public String toString() {
		String s = "v1: " + getEdge1().getVertex1() + ", v2: " + getEdge1().getVertex2() + ", v3: " + getVertexNotOnEdge(edge1);
		return s;
	}

	public Edge getLongestEdge() {
		Edge longest = edge1;
		if (edge1.getLengthSquared() > edge2.getLengthSquared()) {
			if (edge1.getLengthSquared() > edge3.getLengthSquared()) {
				longest = edge1;
			} else {
				longest = edge3;
			}
		} else { // edge2 is longer than edge1
			if (edge2.getLengthSquared() > edge3.getLengthSquared()) {
				longest = edge2;
			} else {
				longest = edge3;
			}
		}
		return longest;
	}

}
